[ 筆記 ] JavaScript 進階 07 - Prototype Chain


Posted by krebikshaw on 2020-09-27

繼承機制

要來了解 Prototype 之前,要先了解什麼是「繼承機制」,以及為什麼要有「繼承機制」。

繼承機制可以把它想像成「傳承」的概念,就像人類的發展需要靠知識的傳承一樣。人類之所以可以傳承知識,就是因為有前人把知識給「紀錄」下來,讓後人可以透過這些「紀錄」快速的學習到知識。如果少了這些「紀錄」,每個人出生下來就只能靠著自己個人的體會來認識這個世界,發展就會非常緩慢。

高中大家在學習的時候,都會有自己做筆記的方式,雖然每個人的筆記都不相同,但都是「繼承」於老師所傳遞的知識,而老師們的知識,又會是「繼承」於教育部所制定的課綱

如果可以把「資源」有效的繼承下來,那每次要利用這個資源的時候,就只需要把要處理的事情,都指派給同一個資源,讓它幫你處理。

如果教室前面有三個神奇寶箱(國文寶箱、英文寶箱、數學寶箱),你只要把題目放入對的寶箱當中,他就會幫你把答案寫好並還給你。請問你手上有一堆題目要寫,你會選擇把題目放入對應的寶箱,還是會自己親手造一個寶箱?

這個問題有點極端,但是我想表達的意思就是,要如何有效率的去應用你所擁有的「資源」。假如課本都已經告訴你可以利用畢氏定理來求出三角形的邊長,你就只需要把手上的題目「指向」解題的公式,沒必要自己開始從頭推導數學公式。

換句話說:
哪天你發現自己的筆記有地方漏寫了,你知道要去老師那邊找,因為你的筆記是從老師那邊「繼承」的,所以老師如果也沒有答案的話,就接著去教育部的課綱裡面找,因為老師的知識也是「繼承」於教育部課綱的。

JavaScript 的繼承機制

JavaScript 不像其他程式語言有 Class 的機制,JavaScript 用了不同的方式實作出「繼承」的功能,而這個功能讓我們可以在不同的物件之間,共享物件的屬性及方法(property & method)

舉例來說:

function Person(name) {
  this.name = name;
  this.getName = function(){
    return this.name;
  }
}

const nick = new Person('nick');
const peter = new Person('peter');
console.log(nick.getName === peter.getName); // => false

儘管兩個 instance 都是回傳 function(){ return this.name; } 內容,但因為 instance 的記憶體位置不同,會被視為不同的方法。

一但 instance 越來越多,需要建立的方法也會越來越多,可能會造成記憶體空間的浪費,為了要貫徹「共享」的概念,我們希望底下的 instance 可以一起使用相同的 getName 方法

Prototype 共用方法

當我們建立好一個資源,並且想把它的方法提供給大家共享時,可以把這個方法建立在 prototype 底下

  • Person.prototype.getName
function Person(name) {
  this.name = name
}

// 想要提供給大家的方法,建立在 prototype 底下
Person.prototype.getName = function() {
  return this.name;
}

只要是「繼承」這個資源的物件,就可以共享 prototype 提供的方法

  • Peter.getName
const peter = new Person('peter');
console.log(Peter.getName)  // 這邊直接用了 Person.prototype 提供的 getName

所以就算有更多的 instance,也都會共享同一個方法

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  return this.name;
}

const nick = new Person('nick');
const peter = new Person('peter');
console.log(nick.getName === peter.getName); // => true

因為方法是「共享」的,所以每當要需要使用到方法時,都一定要透過一個「路徑」去尋找到這個方法,才能順利的使用它

  • 就像 Peter.getName 需要知道這個 getName 的方法要去哪裡找,找到了才能使用它
  • 它找到了這個 getName 方法,是由 Person.prototype 提供的

而這個找尋方法的「路徑」,就是所謂的 Prototype Chain 原型鍊

Prototype Chain 原型鍊

當學生「繼承」了老師的知識時,老師只要需要把解題的訣竅放在 prototype 底下,就可以讓學生來共享這份方法,

  • 老師.prototype.畢氏定理

哪天我們發現自己的筆記裡沒寫到這個方法時,就去老師那裡找。這個找尋方法的路徑,就是透過 __proto__ 來實現的。

  • 學生.__proto__ 可以找到老師那裡提供的方法
  • 學生.__proto__ === 老師.prototype 就可以找到畢氏定理

如果連老師那裡都沒有找到這個方法時,就再到教育部課綱去找

  • 學生.__proto__.__proto__ 可以找到教育部課綱裡提供的方法
  • 學生.__proto__.__proto__ === 教育部課綱.prototype

在使用 new 的同時,instance (peter) 會自動加上 .proto,並且指向建構函式 (Person) 的 .prototype

回到這段程式碼:

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  return this.name;
}

當我們做了 new 的動作時:

  • const nick = new Person('nick');
  • const peter = new Person('peter');
peter.__proto__ 就會指向 => Person.prototype 
nick.__proto__ 就會指向 => Person.prototype

而建構函式 Person 其實也是 Object new 出來的啊,所以 Person.prototype 也是會有 __proto__ 同樣指向 Object.prototype

1. Person 是 function 的 instance
    => 可以想像成是 var Person = new Function();
    => Person.__proto__ === Function.prototype
2. function 是 Object 的 instance
    => 可以想像成是 var Function = new Object();
    => Function.prototype.__proto === Object.prototype
    => Person.__proto__.__proto__ === Object.prototype

所以可以看得出來, peter (instance) 的原型會指向 Person (constructor),而 Person (constructor) 的原型又會指向 Object,這樣鍊型的關係達到了繼承的效果,而又稱為原型鍊。

Prototype Chain : 尋找的順序

console.log(nick.getName());

所以要怎麼找到 getName 這個 method 呢?

1. nick
2. nick.__proto__
   ( = Person.prototype )
3. nick.__proto__.__proto__
   ( = Person.prototype.__proto__ )
   ( = Object.prototype )
4. nick.__proto__.__proto__.__proto__ => 最上層,也就是 null
   ( = Person.prototype.__proto__.__proto__ )
   ( = Object.prototype.__proto__ )

所以 JS 會一直往上找,一但找到該 method 就會停止。
如果 Person 跟 Object 的 prototype 都有 getName,真正回傳的會是 Person的 getName,因為 Person 的尋找優先順序比較高。

所以這樣 一層一層往上查找,就構成一條原型鍊 prototype chain,而這條鍊的最頂層就是 Object.prototype.__proto__ 也就是 null。

注意: .protp 這個指引的屬性在實作上可以省略,這邊為了說明 JS 底層實作的概念所以都有寫出來,實務上盡量不要去寫到 .proto 這個屬性。

可以再看下一個例子,確定自己清楚 prototyp 的尋找順序:

function Person(name) {
  this.name = name;
  this.sayHi = function () {
    console.log(this.name, 'Hi => method of instance'); // => new 出幾份 instance 就會有幾份,佔用記憶體空間
  }
}

Person.prototype.sayHello = function () {
  console.log(this.name, 'Hello => method of Class'); // => 用 prototype 實現共享 property & method
}

Object.prototype.sayHello = function () {
  console.log(this.name, 'Hello => method of Object'); // => method 跟 Person 重複,所以找到 Person 就停了
}

Object.prototype.sayYo = function () {
  console.log(this.name, 'Yo  => method of Object');
}
const peter = new Person('peter');

peter.sayHi(); // peter Hi => method of instance
peter.sayHello(); // peter Hello => method of Class
peter.sayYo(); // peter Yo  => method of Object

// => 這樣才可以使用到 Object 的 method
Object.sayHello.call(peter); // peter Hello => method of Object

好用內建函式

  • hasOwnProperty: 可以確認是否為 instance 自己的方法
    // 如果是自己 instance 的方法就會回傳 true 
     peter.hasOwnProperty(sayHi); // => true
    // 如果不是自己 instance 的方法就會回傳 false
     peter.hasOwnProperty(sayHello); // => false
    
  • instanceof: 確認 A 是否為 B 的 instance
    peter instanceof Person; // true
    Person instanceof Function; // true
    Person instanceof Object; // true
    // Function 跟 Object 互為對方的 intance,好奇妙
    Function instanceof Object; // true 
    Object instanceof Function; // true
    

最後一個概念:

每一個 prototype 都有個 constructor 屬性,而想當然爾就是指向自己(建構函式)囉。

peter.constructor === Person; // true;
Person.prototype.constructor === Person; // true
Person.prototype.hasOwnProperty('constructor'); // true

了解 Prototype Chain 的概念以後,就能理解 JavaScript 是如何實作出「繼承」的功能了。

而這個繼承的功能,在 OOP 物件導向的應用上更能顯現出它的功效,物件導向的部分會在其他文章介紹到。


#Prototype Chain







Related Posts

Advanced JS  (上)

Advanced JS (上)

申請 Facebook 快速登入會員功能

申請 Facebook 快速登入會員功能

The introduction and difference between class component and function component in React

The introduction and difference between class component and function component in React


Comments